﻿using System;
using System.Collections;
using System.Collections.Generic;

namespace NetAssist.Collections
{
  /// <summary>
  /// Represents a bi directional collection of keys and values.
  /// </summary>
  /// <typeparam name="T1">The first type of elements in the map.</typeparam>
  /// <typeparam name="T2">The second type of elements in the map.</typeparam>
  public class Map<T1, T2> : ICollection<KeyValuePair<T1, T2>>
  {
    #region Declarations

    private Dictionary<T1, T2> fwd;
    private Dictionary<T2, T1> rev;
    private object locker = new object();

    #endregion

    #region Constructors

    /// <summary>
    /// Initializes a new instance of the  NetAssist.Collections.Map&lt;T1, T2&gt; class that is empty, has the default initial capacity, and uses the default equality comparer for the key and value types.
    /// </summary>   
    public Map()
    {
      fwd = new Dictionary<T1, T2>();
      rev = new Dictionary<T2, T1>();
    }

    /// <summary>
    /// Initializes a new instance of the NetAssist.Collections.Map&lt;T1, T2&gt; class that contains elements copied from the specified System.Collections.Generic.ICollection&lt;KeyValuePair&lt;T1, T2&gt;&gt;
    /// and uses the default equality comparer for the key type.
    /// </summary>       
    /// <param name="collection">The System.Collections.Generic.ICollection&lt;KeyValuePair&lt;T1, T2>> whose elements are copied to the new Map&lt;T1, T2></param>
    /// <exception cref="System.ArgumentNullException">collection is null.</exception>
    /// <exception cref="System.ArgumentException">collection contains one or more duplicate keys or values.</exception>   
    public Map(ICollection<KeyValuePair<T1, T2>> collection)
      : this(collection.Count)
    {
      Helper.InvalidateNullParam(collection, "collection");
      foreach (var item in collection)
        this.Add(item.Key, item.Value);
    }

    /// <summary>
    /// Initializes a new instance of the NetAssist.Collections.Map&lt;T1, T2> class that is empty, has the default initial capacity, 
    /// and uses the specified System.Collections.Generic.IEqualityComparer&lt;T&gt;.
    /// </summary>
    /// <param name="keyComparer">The System.Collections.Generic.IEqualityComparer&lt;T&gt; implementation to use when comparing keys, or null to use the default System.Collections.Generic.EqualityComparer&lt;T&gt; for the type of the key.</param>
    /// <param name="valueComparer">The System.Collections.Generic.IEqualityComparer&lt;T&gt; implementation to use when comparing values, or null to use the default System.Collections.Generic.EqualityComparer&lt;T&gt; for the type of the value.</param>
    public Map(IEqualityComparer<T1> keyComparer, IEqualityComparer<T2> valueComparer)
    {
      fwd = new Dictionary<T1, T2>(keyComparer);
      rev = new Dictionary<T2, T1>(valueComparer);
    }

    /// <summary>
    /// Initializes a new instance of the NetAssist.Collections.Map&lt;T1, T2> class that is empty, has the specified initial capacity, and uses the default equality comparer for the key type.
    /// </summary>
    /// <param name="capacity">The initial number of elements that the  NetAssist.Collections.Map&gt;T1, T2&lt; can contain.</param>
    /// <exception cref="System.ArgumentOutOfRangeException">capacity is less than 0.</exception>
    public Map(int capacity)
    {
      Helper.InvalidateMinValue(capacity, 0, "capacity");
      fwd = new Dictionary<T1, T2>(capacity);
      rev = new Dictionary<T2, T1>(capacity);
    }

    /// <summary>
    /// Initializes a new instance of the NetAssist.Collections.Map&lt;T1, T2> class that contains elements copied from the specified System.Collections.Generic.ICollection&lt;KeyValuePair&lt;T1, T2&gt;&gt;
    /// and uses the specified System.Collections.Generic.IEqualityComparer&lt;T1> and System.Collections.Generic.IEqualityComparer&lt;T2>.
    /// </summary>
    /// <param name="collection">The System.Collections.Generic.ICollection&lt;KeyValuePair&lt;T1, T2>> whose elements are copied to the new Map&lt;T1, T2&gt;</param>    
    /// <param name="keyComparer">The System.Collections.Generic.IEqualityComparer&lt;T&gt; implementation to use when comparing keys, or null to use the default System.Collections.Generic.EqualityComparer&lt;T&gt; for the type of the key.</param>
    /// <param name="valueComparer">The System.Collections.Generic.IEqualityComparer&lt;T&gt; implementation to use when comparing values, or null to use the default System.Collections.Generic.EqualityComparer&lt;T&gt; for the type of the value.</param>
    /// <exception cref="System.ArgumentNullException">collection is null.</exception>
    /// <exception cref="System.ArgumentException">collection contains one or more duplicate keys or values.</exception>
    public Map(ICollection<KeyValuePair<T1, T2>> collection, IEqualityComparer<T1> keyComparer, IEqualityComparer<T2> valueComparer)
      : this(collection.Count, keyComparer, valueComparer)
    {
      Helper.InvalidateNullParam(collection, "collection");
      foreach (var item in collection)
        this.Add(item.Key, item.Value);
    }

    /// <summary>
    /// Initializes a new instance of the NetAssist.Collections.Map &lt;T1, T2&gt; class that is empty, has the specified initial capacity, and uses the specified
    /// System.Collections.Generic.IEqualityComparer&lt;T1&gt; and ystem.Collections.Generic.IEqualityComparer&lt;T2&gt;.
    /// </summary>
    /// <param name="capacity">The initial number of elements that the  NetAssist.Collections.Map&lt;T1, T2&gt; can contain.</param>
    /// <param name="keyComparer">The System.Collections.Generic.IEqualityComparer&lt;T&gt; implementation to use when comparing keys, or null to use the default System.Collections.Generic.EqualityComparer&lt;T&gt; for the type of the key.</param>
    /// <param name="valueComparer">The System.Collections.Generic.IEqualityComparer&lt;T&gt; implementation to use when comparing values, or null to use the default System.Collections.Generic.EqualityComparer&lt;T&gt; for the type of the value.</param>
    /// <exception cref="System.ArgumentOutOfRangeException">capacity is less than 0.</exception>
    public Map(int capacity, IEqualityComparer<T1> keyComparer, IEqualityComparer<T2> valueComparer)
    {
      fwd = new Dictionary<T1, T2>(capacity, keyComparer);
      rev = new Dictionary<T2, T1>(fwd.Count, valueComparer);
    }

    /// <summary>
    /// Gets the System.Collections.Generic.IEqualityComparer&lt;T1&gt; that is used to determine equality of keys for the map.
    /// </summary>
    /// <returns>
    /// The System.Collections.Generic.IEqualityComparer&lt;T1&gt; generic interface implementation that is used to determine equality of keys for the current NetAssist.Collections.Map&lt;T1, T2&gt;
    /// and to provide hash values for the keys.
    /// </returns>
    public IEqualityComparer<T1> KeyComparer { get { return fwd.Comparer; } }

    /// <summary>
    /// Gets the System.Collections.Generic.IEqualityComparer&lt;T2&gt; that is used to determine equality of values for the map.
    /// </summary>
    /// <returns>
    /// The System.Collections.Generic.IEqualityComparer&lt;T2&gt; generic interface implementation that is used to determine equality of keys for the current NetAssist.Collections.Map&gt;T1, T2&gt;
    /// and to provide hash values for the values.
    /// </returns>
    public IEqualityComparer<T2> ValueComparer { get { return rev.Comparer; } }

    #endregion

    #region Map Methods

    /// <summary>
    /// Adds the specified key and value to the map.
    /// </summary>
    /// <param name="key">The key of the element to add.</param>
    /// <param name="value">The value of the element to add.</param>
    /// <exception cref="System.ArgumentNullException">key is null.</exception>
    /// <exception cref="System.ArgumentNullException">value is null.</exception>
    /// <exception cref="System.ArgumentException">An element with the same key already exists in the map.</exception>
    public void Add(T1 key, T2 value)
    {
      Helper.InvalidateNullParam(key, "key");
      Helper.InvalidateNullParam(value, "value");
      lock (locker)
      {
        fwd.Add(key, value);
        try
        {
          rev.Add(value, key);
        }
        catch (Exception)
        {
          fwd.Remove(key);
          throw;
        }
      }
    }

    /// <summary>
    /// Determines whether the map contains the specified key.
    /// </summary>
    /// <param name="key">The key to locate in the map.</param>
    /// <returns>true if the map contains an element with the specified key; otherwise, false.</returns>
    /// <exception cref="System.ArgumentNullException">key is null.</exception>
    public bool ContainsKey(T1 key)
    {
      Helper.InvalidateNullParam(key, "key");
      return fwd.ContainsKey(key);
    }

    /// <summary>
    /// Determines whether the map contains the specified value.
    /// </summary>
    /// <param name="value">The value to locate in the map.</param>
    /// <returns>true if the map contains an element with the specified value; otherwise, false.</returns>
    /// <exception cref="System.ArgumentNullException">value is null.</exception>
    public bool ContainsValue(T2 value)
    {
      Helper.InvalidateNullParam(value, "value");
      return rev.ContainsKey(value);
    }

    /// <summary>
    /// Gets a collection containing the Keys in the map.
    /// </summary>
    /// <returns>A Collection containing the Keys in the map.</returns>    
    public ICollection<T1> Keys
    {
      get { return fwd.Keys; }
    }

    /// <summary>
    /// Gets a collection containing the Values in the map.
    /// </summary>
    /// <returns>A Collection containing the Values in the map.</returns>    
    public ICollection<T2> Values
    {
      get { return fwd.Values; }
    }

    /// <summary>
    /// Removes the element with the specified key from the map.
    /// </summary>
    /// <param name="key">The key of the element to remove.</param>
    /// <returns>true if the element is successfully found and removed; otherwise, false.</returns>
    /// <exception cref="System.ArgumentNullException">key is null.</exception>
    public bool Remove(T1 key)
    {
      Helper.InvalidateNullParam(key, "key");
      lock (locker)
        if (fwd.ContainsKey(key))
          return rev.Remove(fwd[key]) && fwd.Remove(key);
        else
          return false;
    }

    /// <summary>
    /// Removes the element with the specified value from the map.
    /// </summary>
    /// <param name="value">The value of the element to remove.</param>
    /// <returns>true if the element is successfully found and removed; otherwise, false.</returns>
    /// <exception cref="System.ArgumentNullException">value is null.</exception>
    public bool Remove(T2 value)
    {
      Helper.InvalidateNullParam(value, "value");
      lock (locker)
        if (rev.ContainsKey(value))
          return fwd.Remove(rev[value]) && rev.Remove(value);
        else
          return false;
    }

    /// <summary>
    /// Gets the value associated with the specified key.
    /// </summary>
    /// <param name="key">The key of the value to get.</param>
    /// <param name="value">
    /// When this method returns, contains the value associated with the specified key, if the key is found; otherwise, 
    /// the default value for the type of the value parameter. This parameter is passed uninitialized.
    /// </param>
    /// <returns>true if the map contains an element with the specified key; otherwise, false.</returns>
    /// <exception cref="System.ArgumentNullException">key is null.</exception>
    public bool TryGetValue(T1 key, out T2 value)
    {
      Helper.InvalidateNullParam(key, "key");
      return this.fwd.TryGetValue(key, out value);
    }

    /// <summary>
    ///  Gets the key associated with the specified value.
    /// </summary>
    /// <param name="value">The value of the key to get.</param>
    /// <param name="key">
    /// When this method returns, contains the key associated with the specified value, if the value is found; otherwise, 
    /// the default value for the type of the key parameter. This parameter is passed uninitialized.
    /// </param>
    /// <returns>true if the map contains an element with the specified value; otherwise, false.</returns>
    /// <exception cref="System.ArgumentNullException">value is null.</exception>
    public bool TryGetKey(T2 value, out T1 key)
    {
      Helper.InvalidateNullParam(value, "value");
      return this.rev.TryGetValue(value, out key);
    }

    /// <summary>
    /// Gets or sets the value associated with the specified key.
    /// </summary>
    /// <param name="key">The key of the value to get or set.</param>
    /// <returns>
    /// The value associated with the specified key. If the specified key is not
    /// found, a get operation throws a System.Collections.Generic.KeyNotFoundException,
    /// and a set operation creates a new element with the specified key.
    /// </returns>
    /// <exception cref="System.ArgumentNullException">key is null.</exception>
    /// <exception cref="System.Collections.Generic.KeyNotFoundException">The property is retrieved and key does not exist in the collection.</exception>
    public T2 this[T1 key]
    {
      get
      {
        Helper.InvalidateNullParam(key, "key");
        return this.fwd[key];
      }
      set
      {
        Helper.InvalidateNullParam(key, "key");
        lock (locker)
        {
          if (fwd.ContainsKey(key))
            if (object.Equals(fwd[key], value))
              return;
            else
              this.Remove(key);

          this.Add(key, value);
        }
      }
    }

    /// <summary>
    /// Gets or sets the key associated with the specified Value.
    /// </summary>
    /// <param name="Value">The Value of the key to get or set.</param>
    /// <returns>
    /// The key associated with the specified Value. If the specified Value is not
    /// found, a get operation throws a System.Collections.Generic.KeyNotFoundException,
    /// and a set operation creates a new element with the specified Value.
    /// </returns>
    /// <exception cref="System.ArgumentNullException">Value is null.</exception>
    /// <exception cref="System.Collections.Generic.KeyNotFoundException">The property is retrieved and key does not exist in the collection.</exception>
    public T1 this[T2 Value]
    {
      get
      {
        Helper.InvalidateNullParam(Value, "Value");
        return this.rev[Value];
      }
      set
      {
        Helper.InvalidateNullParam(Value, "Value");
        lock (locker)
        {
          if (rev.ContainsKey(Value))
            if (object.Equals(rev[Value], value))
              return;
            else
              this.Remove(Value);

          this.Add(value, Value);
        }
      }
    }

    #endregion

    #region ICollection<KeyValuePair<T1,T2>> Members

    /// <summary>
    ///Removes all elements from the map.
    /// </summary>
    public void Clear()
    {
      lock (locker)
      {
        this.fwd.Clear();
        this.rev.Clear();
      }
    }

    /// <summary>
    /// Adds the specified key and value to the map.
    /// </summary>
    /// <param name="item">The key-value pair to add</param>
    /// <exception cref="System.ArgumentNullException">The value item is null.</exception>
    /// <exception cref="System.ArgumentNullException">The Key in item is null.</exception>
    /// <exception cref="System.ArgumentNullException">The value in item is null.</exception>
    /// <exception cref="System.ArgumentException">An element with the same key or value already exists in the map.</exception>
    public void Add(KeyValuePair<T1, T2> item)
    {
      this.Add(item.Key, item.Value);
    }

    /// <summary>
    /// Adds the specified key and value to the map.
    /// </summary>
    /// <param name="item">The key-value pair to add</param>
    /// <exception cref="System.ArgumentNullException">The value item is null.</exception>
    /// <exception cref="System.ArgumentNullException">The Key in item is null.</exception>
    /// <exception cref="System.ArgumentNullException">The value in item is null.</exception>
    /// <exception cref="System.ArgumentException">An element with the same key or value already exists in the map.</exception>
    public void Add(KeyValuePair<T2, T1> item)
    {
      this.Add(item.Value, item.Key);
    }

    /// <summary>
    /// Determines whether the Map&lt;T1, T2&gt; contains a pair specified by T1 and T2.
    /// </summary>
    /// <param name="item">The KeyValuePair to locate in the Map&lt;T1, T2&gt;.</param>
    /// <returns>true if the Map&lt;T1, T2&gt; contains a pair specified by T1 and T2; otherwise false.</returns>
    /// <exception cref="System.ArgumentNullException">The Key in item is null.</exception>
    /// <exception cref="System.ArgumentNullException">The value item is null.</exception>    
    public bool Contains(KeyValuePair<T1, T2> item)
    {
      return this.fwd.ContainsKey(item.Key) && this.rev.ContainsKey(item.Value) && this.KeyComparer.Equals(this.rev[item.Value], item.Key);
    }

    /// <summary>
    /// Determines whether the Map&lt;T1, T2&gt; contains a pair specified by T2 and T1.
    /// </summary>
    /// <param name="item">The KeyValuePair to locate in the Map&lt;T1, T2&gt;.</param>
    /// <returns>true if the Map&lt;T1, T2&gt; contains a pair specified by T2 and T1; otherwise false.</returns>
    /// <exception cref="System.ArgumentNullException">The value item is null.</exception>
    /// <exception cref="System.ArgumentNullException">The Key in item is null.</exception>
    public bool Contains(KeyValuePair<T2, T1> item)
    {
      return this.rev.ContainsKey(item.Key) && this.fwd.ContainsKey(item.Value) && this.KeyComparer.Equals(this.rev[item.Key], item.Value); ;
    }

    /// <summary>
    /// Copies the Map&lt;T1, T2&gt;. elements as KeyValuePairs to an existing one-dimensional System.Array, starting at the specified array index.
    /// </summary>
    /// <param name="array">The one-dimensional System.Array that is the destination of the elements copied from Map&lt;T1, T2&gt;.ValueCollection. The System.Array must have zero-based indexing.</param>
    /// <param name="arrayIndex">The zero-based index in array at which copying begins.</param>
    /// <exception cref="System.ArgumentNullException">array is null.</exception>
    /// <exception cref="System.ArgumentOutOfRangeException">index is less than zero.</exception>
    /// <exception cref="System.ArgumentException">The number of elements in the source Map&lt;T1, T2&gt;. is greater than the available space from index to the end of the destination array.</exception>
    public void CopyTo(KeyValuePair<T1, T2>[] array, int arrayIndex)
    {
      ((ICollection)fwd).CopyTo(array, arrayIndex);
    }

    /// <summary>
    /// Gets the number of elements contained in the Map&lt;T1, T2&gt;.
    /// </summary>
    /// <returns>The number of elements contained in the Map&lt;T1, T2&gt;.</returns>
    public int Count
    {
      get { return this.fwd.Count; }
    }

    /// <summary>
    /// Removes the element with the specified key/value from the Map&lt;T1, T2&gt;.
    /// </summary>
    /// <param name="item">The element to remove.</param>
    /// <returns>true if the element is successfully found and removed; otherwise, false. This method returns false if the elements is not found in the Map&lt;T1, T2&gt;.</returns>
    public bool Remove(KeyValuePair<T1, T2> item)
    {
      return this.Remove(item.Key);
    }

    /// <summary>
    /// Removes the element with the specified key/value from the Map&lt;T1, T2&gt;.
    /// </summary>
    /// <param name="item">The element to remove.</param>
    /// <returns>true if the element is successfully found and removed; otherwise, false. This method returns false if the elements is not found in the Map&lt;T1, T2&gt;.</returns>
    public bool Remove(KeyValuePair<T2, T1> item)
    {
      return this.Remove(item.Value);
    }

    /// <summary>
    /// Returns false, Map&lt;T1, T2&gt; is not a read only implementation. 
    /// </summary>
    public bool IsReadOnly
    {
      get { return false; }
    }

    #endregion

    #region IEnumerable<KeyValuePair<T1,T2>> Members

    /// <summary>
    /// Returns an enumerator that iterates through the Map&lt;T1, T2&gt;.
    /// </summary>
    /// <returns>A Map&lt;T1, T2&gt;.Enumerator structure for the Map&lt;T1, T2&gt;.</returns>
    public IEnumerator<KeyValuePair<T1, T2>> GetEnumerator()
    {
      return this.fwd.GetEnumerator();
    }

    #endregion

    #region IEnumerable Members

    IEnumerator IEnumerable.GetEnumerator()
    {
      return this.GetEnumerator();
    }

    #endregion
  }
}
